Class 7 - Platformer Part 1: Advanced Physics
本节课我们将开始一个新的项目:2D 平台跳跃游戏(Platformer),类似于经典的《超级马里奥》。我们将从零开始,完成项目的基本设置、角色 Prefab 的创建,并编写核心的角色移动与瞄准脚本。
Class 7 Overview
- 项目与素材设置
- 核心游戏对象(Prefabs)的创建
- 基础关卡布局
- 编写玩家移动脚本 (Update vs FixedUpdate)
- 实现坠落重玩机制 (Trigger)
- 实现鼠标瞄准系统
1. 项目与素材设置
1.1 场景与素材导入
-
创建新场景:在项目中新建一个场景,命名为
Q2 -
导入素材:将本节课所需素材 (items_spritesheet、tiles_spritesheet、p1_walk、target) 导入到 Assests/Textures/Platformer/ 路径下
-
选中 items_spritesheet、tiles_spritesheet、p1_walk,在 Inspector 中进行如下统一设置:
| 属性 | 设置 |
|---|---|
| Sprite Mode | Multiple |
| Pixels Per Unit (PPU) | 69 |
| Filter Mode | Bilinear |
| Compression | None |
然后点击 Sprite Editor -> Slice -> Type: Automatic -> Apply
- 选中 target(准心),设置如下:
| 属性 | 设置 |
|---|---|
| Sprite Mode | Single |
| Pixels Per Unit (PPU) | 980 |
| Filter Mode | Bilinear |
| Compression | None |
1.2 图层与物理设置
-
添加图层 (Layers):
在Inspector窗口中,点击Layers -> Edit Layers,新增三个图层Ground、PlayerProjectile、Player。 -
设置碰撞矩阵:
前往 Edit -> Project Settings -> Physics 2D,修改碰撞矩阵,取消PlayerProjectile与Player、以及PlayerProjectile与其自身的碰撞交互。
2. 核心游戏对象(Prefabs)的创建
2.1 地面 (Ground) Prefab
- 在场景中创建一个空物体。
- 添加 SpriteRenderer 组件,并赋予一张地块贴图。
- 添加 BoxCollider2D 组件,用于物理碰撞。
- 将其 Layer 设置为
Ground。 - 在 Assets/ 下新建 Prefabs/Platformer/ 文件夹,将该物体从 Hierarchy 拖入,制成 Prefab。
2.2 玩家 (Player) Prefab
重复上述流程,将锁状贴图的物体制作成名为 Target 的 Prefab。
2.3 玩家炮弹 (PlayerProjectile) Prefab
- 创建空物体,添加 SpriteRenderer 组件,赋予主角贴图,并将其 Layer 设置为 Player。
- 添加 CapsuleCollider2D 组件。
- 添加 Rigidbody2D 组件,并展开 Constraints 选项,勾选 Freeze Rotation Z,以防止角色在碰撞后翻倒。
- 将其制作为 Player Prefab。
胶囊体的底部是圆弧形的,这使得角色在平台边缘移动时不容易被卡住,可以获得更平滑的移动体验。
相比之下,盒状碰撞体(BoxCollider2D)的尖角更容易在移动中与地形的边角发生不期望的卡顿。
2.4 子弹 (Projectile) Prefab
- 创建空物体,添加 SpriteRenderer 组件,赋予钥匙贴图(作为子弹),并将其 Layer 设置为 PlayerProjectile。
- 添加 CapsuleCollider2D 组件,并将其 Direction 设置为
Horizontal。 - 添加 Rigidbody2D 组件,并将其 Gravity Scale 设置为 0,让子弹沿直线飞行。
- 将其制作为 Projectile Prefab,并从场景中删除。
DirectionDirection 属性决定了胶囊体的长轴方向。由于我们的子弹是水平飞行的,将其设置为 Horizontal 可以让碰撞体的形状正确匹配贴图,从而实现更精确的碰撞检测。
如果保持默认的 Vertical,碰撞体将是一个圆形。
3. 基础关卡布局
将制作好的 Ground Prefab 拖入场景,搭建一个简单的地形,中间留有间隙。再将 Target Prefab 放置在合适的位置,如下所示可供参考:

4. 编写玩家移动脚本
新建一个 C# 脚本 PlayerController.cs 并挂载到 Player Prefab 上:
using UnityEngine;
public class PlayerController : MonoBehaviour
{
// 移动速度
public float moveSpeed = 18f;
// 刚体组件的引用
private Rigidbody2D _rigidbody2D;
void Start()
{
// 获取并存储 Rigidbody2D 组件
_rigidbody2D = GetComponent<Rigidbody2D>();
}
void Update()
{
// 检测 'A' 键或左箭头键
if (Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.LeftArrow))
{
// 向左施加【冲量】
_rigidbody2D.AddForce(Vector2.left * moveSpeed * Time.deltaTime, ForceMode2D.Impulse);
}
// 检测 'D' 键或右箭头键
if (Input.GetKey(KeyCode.D) || Input.GetKey(KeyCode.RightArrow))
{
// 向右施 加【冲量】
_rigidbody2D.AddForce(Vector2.right * moveSpeed * Time.deltaTime, ForceMode2D.Impulse);
}
}
}
4.1 Update vs FixedUpdate & Force vs Impulse
这是一个非常重要的物理概念。
- Update(): 每帧执行一次。其执行频率受设备性能影响,因此在处理物理移动时需要乘以 Time.deltaTime 来抹平帧率差异。
- FixedUpdate(): 以固定的时间间隔(默认为 0.02s)执行一次,不受帧率影响。所有物理相关的计算都推荐放在这里。
接下来是 AddForce 函数:
AddForce 函数接收两个参数:力和力的模式 (ForceMode2D)。
-
ForceMode2D.Force (默认): 此模式会模拟一个
持续的力,其内部计算会自动乘以 Time.fixedDeltaTime。如果在 Update 中使用,就会导致 moveSpeed _ Time.deltaTime _ Time.fixedDeltaTime 的双重时间缩放,使得最终作用力极小。 -
ForceMode2D.Impulse: 此模式会模拟一个
瞬时冲量,它不会再额外乘以时间。这使得我们可以在 Update 中手动乘以 Time.deltaTime,从而精确地控制每帧施加的冲量大小,避免了双重时间缩放的问题。
输入检测最好可以始终放在Update()中,因为它能最快地响应玩家的每帧操作。物理计算(如 AddForce)理论上应放在FixedUpdate()中,这样可以省略 Time.deltaTime 并且使用默认的 ForceMode2D.Force 模式。
为何本课选择在 Update 中处理物理?
主要为了代码的统一性和简洁性,我们将输入检测和物理响应都放在 Update 中,并且更好的引入这个新概念。然而,为了在这种情况下正确地施加力,我们必须:
- 乘以
Time.deltaTime来保证帧率无关性。 - 使用
ForceMode2D.Impulse来避免物理引擎内部多余的时间缩放。
5. 实现坠落重玩机制 (Trigger)
当玩家掉出平台时,我们需要重新加载当前场景。
5.1 创建死亡区域 (Death Zone)
- 在场景底部创建一个大的空物体。
- 为其添加一个 BoxCollider2D 组件,调整大小以覆盖整个掉落区域。
- 在 BoxCollider2D 组件中,勾选 Is Trigger 选项。
-
Collider (碰撞器):是一个实体障碍物,会产生物理碰撞效果。检测碰撞使用 OnCollisionEnter2D(Collision2D collision)。 -
Trigger (触发器):不是一个实体,物体可以穿过它。它只用于检测是否有物 体进入其范围,而不产生物理碰撞。检测触发使用 OnTriggerEnter2D(Collider2D other)。
5.2 编写触发代码
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Platformer
{
public class LevelBound : MonoBehaviour
{
private void OnTriggerEnter2D(Collider2D other)
{
if(other.gameObject.GetComponent<PlayerController>() != null)
{
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
}
}
}
5.3 添加场景到生成设置
为了让 LoadScene 函数能够找到要加载的场景,我们必须:
- 前往 File -> Build Settings
- 点击 Add Open Scenes 将当前场景(Q2)添加到列表中。